/*
 * Copyright 2019. Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.penguinswim;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Vibrator;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.TextView;
import com.google.android.apps.santatracker.doodles.shared.CallbackProcess;
import com.google.android.apps.santatracker.doodles.shared.EventBus;
import com.google.android.apps.santatracker.doodles.shared.EventBus.EventBusListener;
import com.google.android.apps.santatracker.doodles.shared.Process;
import com.google.android.apps.santatracker.doodles.shared.ProcessChain;
import com.google.android.apps.santatracker.doodles.shared.UIUtil;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.shared.WaitProcess;
import com.google.android.apps.santatracker.doodles.shared.actor.Actor;
import com.google.android.apps.santatracker.doodles.shared.actor.Camera;
import com.google.android.apps.santatracker.doodles.shared.actor.CameraShake;
import com.google.android.apps.santatracker.doodles.shared.actor.RectangularInstructionActor;
import com.google.android.apps.santatracker.doodles.shared.physics.Util;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/** The model for the swimming game. */
public class SwimmingModel implements TiltModel, EventBusListener {
    public static final int LEVEL_WIDTH = 1280;
    public static final int[] SCORE_THRESHOLDS = {30, 50, 100};
    private static final String TAG = SwimmingModel.class.getSimpleName();
    private static final float SHAKE_FREQUENCY = 33;
    private static final float SHAKE_MAGNITUDE = 40;
    private static final float SHAKE_FALLOFF = 0.9f;
    private static final long VIBRATION_DURATION_MS = 50;
    private static final int WORLD_TO_METER_RATIO = 700;
    private static final long WAITING_STATE_DELAY_MS = 1000;
    private static final long COUNTDOWN_DELAY_MS = 4000;
    private static final float COUNTDOWN_BUMP_SCALE = 1.5f;

    public final List<String> collisionObjectTypes;

    public String levelName;
    public boolean collisionMode = true;

    public List<Actor> actors;
    public List<Actor> uiActors; // Will be drawn above actors.
    public Camera camera;
    public CameraShake cameraShake;
    public SwimmerActor swimmer;
    public RectangularInstructionActor instructions;
    public TextView countdownView;
    public ObstacleManager obstacleManager;
    public Vibrator vibrator;
    public Locale locale;
    public int distanceMeters;
    public int currentScoreThreshold = 0;

    public int screenWidth;
    public int screenHeight;
    public Vector2D tilt;

    // Measures the time elapsed in the current state. This value is reset to 0 upon entering a new
    // state.
    public long timeElapsed = 0;
    public int playCount;
    private float countdownTimeMs = 3000;
    private List<ProcessChain> processChains = new ArrayList<>();
    private SwimmingState state;

    public SwimmingModel() {
        tilt = Vector2D.get(0, 0);
        actors = new ArrayList<>();
        uiActors = new ArrayList<>();
        state = SwimmingState.INTRO;

        collisionObjectTypes = new ArrayList<>();
        collisionObjectTypes.addAll(BoundingBoxSpriteActor.TYPE_TO_RESOURCE_MAP.keySet());
    }

    public static int getMetersFromWorldY(float distance) {
        return Math.max(0, (int) (-distance / WORLD_TO_METER_RATIO));
    }

    public static int getWorldYFromMeters(int meters) {
        return -meters * WORLD_TO_METER_RATIO;
    }

    @Override
    public void onEventReceived(int type, Object data) {
        switch (type) {
            case EventBus.VIBRATE:
                if (vibrator != null) {
                    vibrator.vibrate(VIBRATION_DURATION_MS);
                }
                break;

            case EventBus.SHAKE_SCREEN:
                cameraShake.shake(SHAKE_FREQUENCY, SHAKE_MAGNITUDE, SHAKE_FALLOFF);
                break;

            case EventBus.GAME_STATE_CHANGED:
                SwimmingState state = (SwimmingState) data;
                if (state == SwimmingState.WAITING) {
                    long countdownDelayMs = WAITING_STATE_DELAY_MS;
                    if (playCount == 0) {
                        // Wait for the crossfade to finish then show instructions.
                        processChains.add(
                                new WaitProcess(WAITING_STATE_DELAY_MS)
                                        .then(
                                                new CallbackProcess() {
                                                    @Override
                                                    public void updateLogic(float deltaMs) {
                                                        if (getState() == SwimmingState.WAITING) {
                                                            instructions.show();
                                                        }
                                                    }
                                                })
                                        .then(
                                                new WaitProcess(COUNTDOWN_DELAY_MS)
                                                        .then(
                                                                new CallbackProcess() {
                                                                    @Override
                                                                    public void updateLogic(
                                                                            float deltaMs) {
                                                                        instructions.hide();
                                                                    }
                                                                })));
                        // If we're showing the instructions, wait until the instructions is hidden
                        // before
                        // starting the countdown.
                        countdownDelayMs += COUNTDOWN_DELAY_MS + 300;
                    }
                    // Start countdown.
                    processChains.add(
                            new WaitProcess(countdownDelayMs)
                                    .then(
                                            new Process() {
                                                @Override
                                                public void updateLogic(float deltaMs) {
                                                    final float oldCountdownTimeMs =
                                                            countdownTimeMs;
                                                    float newCountdownTimeMs =
                                                            countdownTimeMs - deltaMs;
                                                    if ((long) newCountdownTimeMs / 1000
                                                            != (long) oldCountdownTimeMs / 1000) {
                                                        countdownView.post(
                                                                new Runnable() {
                                                                    @Override
                                                                    public void run() {
                                                                        countdownView.setVisibility(
                                                                                View.VISIBLE);
                                                                        // Use the old integer value
                                                                        // so that the countdown
                                                                        // goes 3, 2, 1 and not 2,
                                                                        // 1, 0.
                                                                        String countdownValue =
                                                                                NumberFormat
                                                                                        .getInstance(
                                                                                                locale)
                                                                                        .format(
                                                                                                (long)
                                                                                                                oldCountdownTimeMs
                                                                                                        / 1000);
                                                                        setTextAndBump(
                                                                                countdownView,
                                                                                countdownValue);
                                                                    }
                                                                });
                                                    }
                                                    countdownTimeMs = newCountdownTimeMs;
                                                }

                                                @Override
                                                public boolean isFinished() {
                                                    return countdownTimeMs <= 0;
                                                }
                                            })
                                    .then(
                                            new CallbackProcess() {
                                                @Override
                                                public void updateLogic(float deltaMs) {
                                                    countdownView.post(
                                                            new Runnable() {
                                                                @Override
                                                                public void run() {
                                                                    countdownView.setVisibility(
                                                                            View.INVISIBLE);
                                                                }
                                                            });
                                                    setState(SwimmingState.SWIMMING);
                                                }
                                            }));
                } else if (state == SwimmingState.SWIMMING) {
                    swimmer.startSwimming();
                }
        }
    }

    public SwimmingState getState() {
        return state;
    }

    public void setState(SwimmingState state) {
        if (this.state != state) {
            this.state = state;
            timeElapsed = 0;
            EventBus.getInstance().sendEvent(EventBus.GAME_STATE_CHANGED, state);
        }
    }

    public void update(float deltaMs) {
        synchronized (this) {
            timeElapsed += deltaMs;

            ProcessChain.updateChains(processChains, deltaMs);

            for (int i = 0; i < actors.size(); i++) {
                actors.get(i).update(deltaMs);
            }

            for (int i = 0; i < uiActors.size(); i++) {
                uiActors.get(i).update(deltaMs);
            }

            if (state == SwimmingState.SWIMMING || state == SwimmingState.WAITING) {
                swimmer.updateTargetPositionFromTilt(tilt, LEVEL_WIDTH);

                int newDistance = getMetersFromWorldY(swimmer.position.y);
                if (newDistance != distanceMeters) {
                    if (newDistance >= SwimmingLevelChunk.LEVEL_LENGTH_IN_METERS) {
                        swimmer.endGameWithoutCollision();
                    }
                    distanceMeters =
                            Math.min(SwimmingLevelChunk.LEVEL_LENGTH_IN_METERS, newDistance);
                    EventBus.getInstance().sendEvent(EventBus.SCORE_CHANGED, distanceMeters);
                }
                if (swimmer.isDead) {
                    setState(SwimmingState.FINISHED);
                }

                resolveCollisions(deltaMs);

                clampCameraPosition();
            }
        }
    }

    public void clampCameraPosition() {
        if (!SwimmingFragment.editorMode) {
            float swimmerHeight = swimmer.collisionBody.getHeight();
            float minCameraOffset = camera.toWorldScale(screenHeight) - 3.5f * swimmerHeight;
            float maxCameraOffset = camera.toWorldScale(screenHeight) - 4.0f * swimmerHeight;
            camera.position.set(
                    camera.position.x,
                    Util.clamp(
                            camera.position.y,
                            swimmer.position.y - minCameraOffset,
                            swimmer.position.y - maxCameraOffset));
        }
    }

    public void resolveCollisions(float deltaMs) {
        obstacleManager.resolveCollisions(swimmer, deltaMs);
    }

    public void drawActors(Canvas canvas) {
        List<Actor> actorsToDraw = new ArrayList<>(actors);
        actorsToDraw.addAll(obstacleManager.getActors());
        Collections.sort(actorsToDraw);
        for (int i = 0; i < actorsToDraw.size(); i++) {
            actorsToDraw.get(i).draw(canvas);
        }
    }

    public void drawUiActors(Canvas canvas) {
        for (int i = 0; i < uiActors.size(); i++) {
            uiActors.get(i).draw(canvas);
        }
    }

    public int getStarCount() {
        return currentScoreThreshold;
    }

    public void onTouchDown() {
        if (getState() == SwimmingState.SWIMMING) {
            swimmer.diveDown();
        }
    }

    public void createActor(Vector2D position, String objectType, Resources resources) {
        if (BoundingBoxSpriteActor.TYPE_TO_RESOURCE_MAP.containsKey(objectType)) {
            actors.add(BoundingBoxSpriteActor.create(position, objectType, resources));
        }
    }

    public void sortActors() {
        Collections.sort(actors);
    }

    @Override
    public List<Actor> getActors() {
        return actors;
    }

    @Override
    public void addActor(Actor actor) {
        if (actor instanceof SwimmerActor) {
            this.swimmer = (SwimmerActor) actor;
        } else if (actor instanceof Camera) {
            this.camera = (Camera) actor;
        } else if (actor instanceof CameraShake) {
            this.cameraShake = (CameraShake) actor;
        } else if (actor instanceof ObstacleManager) {
            this.obstacleManager = (ObstacleManager) actor;
        }
        actors.add(actor);
        sortActors();
    }

    public void addUiActor(Actor actor) {
        if (actor instanceof RectangularInstructionActor) {
            this.instructions = (RectangularInstructionActor) actor;
        }
        uiActors.add(actor);
    }

    public void setCountdownView(TextView countdownView) {
        this.countdownView = countdownView;
    }

    @Override
    public void setLevelName(String levelName) {
        this.levelName = levelName;
    }

    private void setTextAndBump(final TextView textView, String text) {
        float endScale = textView.getScaleX();
        float startScale = COUNTDOWN_BUMP_SCALE * textView.getScaleX();
        textView.setText(text);
        if (!"Nexus 9".equals(Build.MODEL)) {
            ValueAnimator scaleAnimation =
                    UIUtil.animator(
                            200,
                            new AccelerateDecelerateInterpolator(),
                            new AnimatorUpdateListener() {
                                @Override
                                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                                    float scaleValue =
                                            (float) valueAnimator.getAnimatedValue("scale");
                                    textView.setScaleX(scaleValue);
                                    textView.setScaleY(scaleValue);
                                }
                            },
                            UIUtil.floatValue("scale", startScale, endScale));
            scaleAnimation.start();
        }
    }

    /** States for the swimming game. */
    public enum SwimmingState {
        INTRO,
        WAITING,
        SWIMMING,
        FINISHED,
    }
}
